agentmux_srv\drone\executor\blocks/
condition.rs1use serde_json::{json, Value};
18
19use crate::drone::data_flow::ExecutionScope;
20use crate::drone::types::FlowNode;
21
22pub async fn run(node: &FlowNode, scope: &ExecutionScope) -> Result<Value, String> {
23 let expr = node
24 .data
25 .get("expr")
26 .and_then(|v| v.as_str())
27 .ok_or_else(|| "Condition block missing `expr`".to_string())?;
28 let resolved = scope.resolve(expr);
29 let result = eval_simple(&resolved)?;
30 Ok(json!({ "result": result }))
31}
32
33fn eval_simple(expr: &str) -> Result<bool, String> {
34 let trimmed = expr.trim();
35 if trimmed.eq_ignore_ascii_case("true") {
37 return Ok(true);
38 }
39 if trimmed.eq_ignore_ascii_case("false") {
40 return Ok(false);
41 }
42 let (lhs, op, rhs) = split_binop(trimmed)
43 .ok_or_else(|| format!("could not parse condition: `{trimmed}`"))?;
44 let lhs = lhs.trim().trim_matches(|c| c == '\'' || c == '"');
45 let rhs = rhs.trim().trim_matches(|c| c == '\'' || c == '"');
46
47 if let (Ok(a), Ok(b)) = (lhs.parse::<f64>(), rhs.parse::<f64>()) {
48 return Ok(match op {
49 "==" => (a - b).abs() < f64::EPSILON,
50 "!=" => (a - b).abs() >= f64::EPSILON,
51 "<" => a < b,
52 "<=" => a <= b,
53 ">" => a > b,
54 ">=" => a >= b,
55 _ => unreachable!(),
56 });
57 }
58
59 Ok(match op {
60 "==" => lhs == rhs,
61 "!=" => lhs != rhs,
62 "<" => lhs < rhs,
63 "<=" => lhs <= rhs,
64 ">" => lhs > rhs,
65 ">=" => lhs >= rhs,
66 _ => unreachable!(),
67 })
68}
69
70fn split_binop(s: &str) -> Option<(&str, &str, &str)> {
73 for op in &["==", "!=", "<=", ">=", "<", ">"] {
74 if let Some(idx) = s.find(op) {
75 return Some((&s[..idx], op, &s[idx + op.len()..]));
78 }
79 }
80 None
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::drone::types::NodePosition;
87
88 fn mk(expr: &str) -> FlowNode {
89 FlowNode {
90 id: "c".to_string(),
91 position: NodePosition::default(),
92 data: json!({ "kind": "condition", "expr": expr }),
93 node_type: String::new(),
94 }
95 }
96
97 #[tokio::test]
98 async fn numeric_lt() {
99 let n = mk("3 < 5");
100 let out = run(&n, &ExecutionScope::new()).await.unwrap();
101 assert_eq!(out, json!({ "result": true }));
102 }
103
104 #[tokio::test]
105 async fn string_eq() {
106 let n = mk("'a' == 'a'");
107 let out = run(&n, &ExecutionScope::new()).await.unwrap();
108 assert_eq!(out, json!({ "result": true }));
109 }
110
111 #[tokio::test]
112 async fn rejects_unparseable() {
113 let n = mk("foo bar baz");
114 let r = run(&n, &ExecutionScope::new()).await;
115 assert!(r.is_err());
116 }
117
118 #[tokio::test]
119 async fn bare_true() {
120 let n = mk("true");
121 let out = run(&n, &ExecutionScope::new()).await.unwrap();
122 assert_eq!(out, json!({ "result": true }));
123 }
124
125 #[tokio::test]
126 async fn resolves_var_in_expr() {
127 let n = mk("{{var.count}} >= 10");
128 let mut scope = ExecutionScope::new();
129 scope.vars.insert("count".to_string(), json!(15));
130 let out = run(&n, &scope).await.unwrap();
131 assert_eq!(out, json!({ "result": true }));
132 }
133}